package org.gpp.proj1.map.renderer;

import java.nio.FloatBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.gpp.proj1.map.GameMap;
import org.gpp.proj1.map.MapNode;
import org.gpp.proj1.util.GeometryBatch;
import org.gpp.proj1.util.ModelFactory;
import org.gpp.proj1.util.TextureStateFactory;

import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector3f;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.CullState;
import com.jme.scene.state.TextureState;
import com.jme.system.DisplaySystem;

/**
 * Class that builds a GameMap's node. It groups the
 * map's tiles by area in order to take advantage of
 * JME's node structure, culling unnecessary nodes
 * 
 * @author Paolo Victor, paolovictor@gmail.com
 */
public class MapNodeBuilder {
	/**
	 * The width of the batch tile group. This and the BATCH_HEIGHT
	 * constant are used to determine the number of GeometryBatch and
	 * decoration Node groups
	 */
	private static final int BATCH_WIDTH = 16;
	
	/**
	 * The height of the batch tile group 
	 */
	private static final int BATCH_HEIGHT = 16;
	
	/**
	 * Pretty self explanatory, huh?
	 */
	private static final int BLOCK_SIZE = MapNode.TILE_SIZE;
	
	/**
	 * Decorations with this texture will not receive a texture state. 
	 */
	private static final byte NULL_TEXTURE = -1;
	
	/**
	 * Builds the map's node
	 * @param map
	 * @return
	 */
	public static MapNode buildNode( GameMap map ) {
		MapNode mapNode = new MapNode( "MAP" , map.getHeights(), map.getModifiers(), map.getMapWidth() * BLOCK_SIZE, map.getMapLength() * BLOCK_SIZE);
	
		Node lowerBatches = buildTopBatches( map.getTopTextures(), map.getHeights() );
		Node wallBatches = buildWallBatches( map.getSideTextures(), map.getHeights() );
		Node decorationModels = buildDecorationModels( map );
		
		mapNode.attachChild( lowerBatches );
		mapNode.attachChild( wallBatches );
		mapNode.attachChild( decorationModels );

		// Correcting angles and position
		Quaternion rotation = new Quaternion();
		rotation.fromAngles( - FastMath.PI / 2, 0.0f, 0.0f );
		rotation.normalize();
	
		mapNode.setLocalRotation( rotation );
		mapNode.setLocalTranslation( new Vector3f( BLOCK_SIZE/2, 0.0f, map.getMapLength() * BLOCK_SIZE ) );
		
		mapNode.updateGeometricState( 0.0f, true );
		
		return mapNode;
	}

	/**
	 * Loads the models on their map positions, the models are positioned
	 * on the tile's floor, with center at (0,0,0) and top given by
	 * the positive Z axis.
	 * 
	 * @param map
	 * @return
	 */
	private static Node buildDecorationModels(GameMap map) {
		Node decorationRoot = new Node( "BATCH_ROOT" );
		
		int width = map.getDecorationModels()[0].length;
		int height = map.getDecorationModels().length;
		int nodesWidth = width % BATCH_WIDTH == 0 ? width / BATCH_WIDTH : width / BATCH_WIDTH + 1;
		int nodesHeight = height % BATCH_HEIGHT == 0 ? height / BATCH_HEIGHT : height / BATCH_HEIGHT + 1; 
		
		// Cannot create a generic array of <String, GeometryBatch> =/
		Object[][] nodes = new Object[nodesHeight][nodesWidth];
		
		String[][] decorations = map.getDecorationModels();
		byte[][] decorationTextures = map.getDecorationTextures();
		float[][][] decorationRotations = map.getDecorationRotations();
		float[][] decorationScales = map.getDecorationScales();
		byte[][] heights = map.getHeights();
		
		// Loading models
		for( int x = 0; x < width; x++ ) {
			for( int y = 0; y < height; y++ ) {
				if( decorations[y][x] != null ) {
					int xNode = x/BATCH_WIDTH;
					int yNode = y/BATCH_HEIGHT;
					
					if( nodes[yNode][xNode] == null ) {
						
						nodes[yNode][xNode] = new Node( x + "-" + y );
					}
					
					Node node = (Node)nodes[yNode][xNode];
					
					Spatial model = ModelFactory.getInstance().getModel( decorations[y][x] );
					
					if (decorationTextures[y][x] != MapNodeBuilder.NULL_TEXTURE) {
						model.setRenderState( TextureStateFactory.getInstance().getTextureState( decorationTextures[y][x] ) );
						model.updateRenderState();	
					}					
					
					model.setLocalTranslation( new Vector3f( x * BLOCK_SIZE - xNode * BLOCK_SIZE, y * BLOCK_SIZE - yNode * BLOCK_SIZE,  heights[y][x] * MapNode.TILE_HEIGHT ) );
					
					// Applying rotation
					Quaternion rotation = new Quaternion();
					rotation.fromAngles( decorationRotations[y][x][0], decorationRotations[y][x][1], decorationRotations[y][x][2] );
					rotation.normalize();
					
					model.setLocalRotation( rotation );
					
					// Applying scale
					model.setLocalScale( decorationScales[y][x] );
					
					// Updating state and attaching
					model.updateGeometricState( 0.0f, true );
					
					node.attachChild( model );
				}
			}
		}
		
		// Attaching nodes to the root
		for( int x = 0; x < nodesWidth; x++ ) {
			for( int y = 0; y < nodesHeight; y++ ) {
				Node node = (Node) nodes[y][x];
				if( node != null ) {
					node.setLocalTranslation( new Vector3f( x * BLOCK_SIZE, y * BLOCK_SIZE, 0.0f ) );
					node.updateGeometricState( 0.0f, true );
					
					decorationRoot.attachChild( (Node) nodes[y][x] );
				}
			}
		}
		
		return decorationRoot;
	}

	private static Node buildWallBatches( byte[][] wallTextures, byte[][] heights ) {
		if( wallTextures.length == 0 || wallTextures[0].length == 0 ) {
			return new Node( "BATCH_ROOT" );
		}
		
		Node batchRoot = new Node( "BATCH_ROOT" );
		
		int width = wallTextures[0].length;
		int height = wallTextures.length;
		int batchesWidth = width % BATCH_WIDTH == 0 ? width / BATCH_WIDTH : width / BATCH_WIDTH + 1;
		int batchesHeight = height % BATCH_HEIGHT == 0 ? height / BATCH_HEIGHT : height / BATCH_HEIGHT + 1;
		
		int macroWidth = width * BLOCK_SIZE;
		int macroHeight = height * BLOCK_SIZE;
		
		// Cannot create a generic array of <String, GeometryBatch> =/
		Object[][] batches = new Object[batchesHeight][batchesWidth];
		
		for( int k = 0; k < batchesHeight; k++ ) {
			for( int i = 0; i < batchesWidth; i++ ) {
				batches[k][i] = new HashMap<String, GeometryBatch>();
			}
		}
		
		// Filling the batches
		Quaternion southRotationQuaternion = new Quaternion();	
		southRotationQuaternion.fromAngles( - FastMath.PI / 2, 0, FastMath.PI );
		southRotationQuaternion.normalize();
		
		Quaternion northRotationQuaternion = new Quaternion();
		northRotationQuaternion.fromAngles( - FastMath.PI / 2, 0, 0 );
		northRotationQuaternion.normalize();
		
		Quaternion westRotationQuaternion = new Quaternion();
		westRotationQuaternion.fromAngles( - FastMath.PI / 2, 0, FastMath.PI / 2 );
		westRotationQuaternion.normalize();
		
		Quaternion eastRotationQuaternion = new Quaternion();
		eastRotationQuaternion.fromAngles( - FastMath.PI / 2, 0, - FastMath.PI / 2 );
		eastRotationQuaternion.normalize();
		
		for( int x = 0; x < width; x++ ) {
			for( int y = 0; y < height; y++ ) {
				byte textureId = wallTextures[y][x];
				byte z = heights[y][x];
				
				// Defining the batch
				int xBatch = x/BATCH_WIDTH;
				int yBatch = y/BATCH_HEIGHT;
				Map<String, GeometryBatch> batchMap = (Map<String, GeometryBatch>) batches[yBatch][xBatch];
				
				// South wall
				attachNorthQuad(heights, macroWidth, macroHeight, southRotationQuaternion, x, y, textureId, z, xBatch, yBatch, batchMap);
				attachSouthQuad(heights, macroWidth, macroHeight, northRotationQuaternion, x, y, textureId, z, xBatch, yBatch, batchMap);
				attachWestQuad(heights, macroWidth, macroHeight, westRotationQuaternion, x, y, textureId, z, xBatch, yBatch, batchMap);
				attachEastQuad(heights, macroWidth, macroHeight, eastRotationQuaternion, x, y, textureId, z, xBatch, yBatch, batchMap);
			}
		}
		
		// Attaching batches to root
		for( int y = 0; y < batchesHeight; y++ ) {
			for( int x = 0; x < batchesWidth; x++ ) {				
				Map<Integer, GeometryBatch> batchMap = (Map<Integer, GeometryBatch>) batches[y][x];
				
				Node batchNode = new Node("BATCH_NODE");
				Collection<GeometryBatch> geometryBatches = batchMap.values();
				
				for( GeometryBatch geometryBatch : geometryBatches ) {
					batchNode.attachChild( geometryBatch );
				}
				
				batchNode.setLocalTranslation( new Vector3f( x * BLOCK_SIZE, y * BLOCK_SIZE, 0.0f ) );
				batchNode.updateGeometricState( 0.0f, true );
				batchRoot.attachChild( batchNode );
			}
		}
		
		return batchRoot;
	}

	private static void attachNorthQuad(byte[][] heights, int macroWidth, int macroHeight, Quaternion southRotationQuaternion, int x, int y, byte textureId, byte z, int xBatch, int yBatch, Map<String, GeometryBatch> batchMap) {
		if( y == 0 || z > heights[y - 1][x] ) {
			byte deltaZ = ( y == 0 ) ? z : (byte)(z - heights[y - 1][x]);
			
			Quad south = new Quad( "South" + x + y, BLOCK_SIZE, MapNode.TILE_HEIGHT * deltaZ );
			FloatBuffer buffer = south.getTextureBuffer();
			
			buffer.clear();
			buffer.put(0).put(deltaZ); buffer.put(0).put(0); buffer.put(1).put(0);	buffer.put(1).put(deltaZ);
			buffer.rewind();
			
			if( ! batchMap.containsKey( textureId + "_SOUTH" ) ) {
				GeometryBatch batch = new GeometryBatch( textureId + "_SOUTH", new Box("Base" + textureId,new Vector3f(0,0,0), new Vector3f(0,0,0) ) );
				batch.setLocalRotation( southRotationQuaternion );
				
				TextureState textureState = TextureStateFactory.getInstance().getTextureState( (int)textureId );
				CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
				cullState.setCullMode( CullState.CS_FRONT );
				
				if( textureState != null && cullState != null ) {
					batch.setRenderState( textureState );
					batch.setRenderState( cullState );
					batch.updateRenderState();
				}				
				
				
				batchMap.put( textureId + "_SOUTH", batch );
			}
			
			batchMap.get( textureId + "_SOUTH" ).addGeometry( south,
					new Vector3f( - (x * BLOCK_SIZE - xBatch * BLOCK_SIZE),
							(macroHeight - (z * MapNode.TILE_HEIGHT - MapNode.TILE_HEIGHT * deltaZ / 2 )) - macroHeight,
							( ( y * BLOCK_SIZE + yBatch * BLOCK_SIZE ) - BLOCK_SIZE / 2 ) - macroWidth ) );
		}
	}
	
	private static void attachSouthQuad(byte[][] heights, int macroWidth, int macroHeight, Quaternion northRotationQuaternion, int x, int y, byte textureId, byte z, int xBatch, int yBatch, Map<String, GeometryBatch> batchMap) {
		if( y == heights.length - 1 || z > heights[y + 1][x] ) {
			byte deltaZ = ( y == heights.length - 1 ) ? z : (byte)(z - heights[y + 1][x]);
			
			Quad south = new Quad( "South" + x + y, BLOCK_SIZE, MapNode.TILE_HEIGHT * deltaZ );
			FloatBuffer buffer = south.getTextureBuffer();
			
			buffer.clear();
			buffer.put(0).put(deltaZ); buffer.put(0).put(0); buffer.put(1).put(0); buffer.put(1).put(deltaZ);
			buffer.rewind();
			
			if( ! batchMap.containsKey( textureId + "_NORTH" ) ) {
				GeometryBatch batch = new GeometryBatch( textureId + "_NORTH", new Box("Base" + textureId,new Vector3f(0,0,0), new Vector3f(0,0,0) ) );
				batch.setLocalRotation( northRotationQuaternion );
				
				TextureState textureState = TextureStateFactory.getInstance().getTextureState( (int)textureId );
				CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
				cullState.setCullMode( CullState.CS_FRONT );
				
				if( textureState != null && cullState != null ) {
					batch.setRenderState( textureState );
					batch.setRenderState( cullState );
					batch.updateRenderState();
				}
				
				batchMap.put( textureId + "_NORTH", batch );
			}
			
			batchMap.get( textureId + "_NORTH" ).addGeometry( south,
					new Vector3f( x * BLOCK_SIZE - xBatch * BLOCK_SIZE,
							- (z * MapNode.TILE_HEIGHT - MapNode.TILE_HEIGHT  * deltaZ / 2 ),
							(macroWidth - ( y * BLOCK_SIZE + yBatch * BLOCK_SIZE ) ) - BLOCK_SIZE / 2 ) );
		}
	}
	
	private static void attachWestQuad(byte[][] heights, int macroWidth, int macroHeight, Quaternion westRotationQuaternion, int x, int y, byte textureId, byte z, int xBatch, int yBatch, Map<String, GeometryBatch> batchMap) {
		if( x == 0 || z > heights[y][x - 1] ) {
			byte deltaZ = ( x == 0 ) ? z : (byte)(z - heights[y][x - 1]);
			
			Quad south = new Quad( "West" + x + y, BLOCK_SIZE, MapNode.TILE_HEIGHT * deltaZ );
			FloatBuffer buffer = south.getTextureBuffer();
			
			buffer.clear();
			buffer.put(0).put(deltaZ);
			buffer.put(0).put(0);
			buffer.put(1).put(0);
			buffer.put(1).put(deltaZ);
			buffer.rewind();
			
			if( ! batchMap.containsKey( textureId + "_WEST" ) ) {
				GeometryBatch batch = new GeometryBatch( textureId + "_WEST", new Box("Base" + textureId,new Vector3f(0,0,0), new Vector3f(0,0,0) ) );
				batch.setLocalRotation( westRotationQuaternion );
				
				TextureState textureState = TextureStateFactory.getInstance().getTextureState( (int)textureId );
				CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
				cullState.setCullMode( CullState.CS_BACK );
				
				if( textureState != null && cullState != null ) {
					batch.setRenderState( textureState );
					batch.setRenderState( cullState );
					batch.updateRenderState();
				}
				
				batchMap.put( textureId + "_WEST", batch );
			}
			
			batchMap.get( textureId + "_WEST" ).addGeometry( south,
					new Vector3f( macroWidth - (y * BLOCK_SIZE + yBatch * BLOCK_SIZE ),
								  (macroHeight - (z * MapNode.TILE_HEIGHT - MapNode.TILE_HEIGHT  * deltaZ / 2 )) - macroHeight,
								  -(x * BLOCK_SIZE - xBatch * BLOCK_SIZE) + BLOCK_SIZE /2 ) );
		}
	}
	
	private static void attachEastQuad(byte[][] heights, int macroWidth, int macroHeight, Quaternion eastRotationQuaternion, int x, int y, byte textureId, byte z, int xBatch, int yBatch, Map<String, GeometryBatch> batchMap) {
		if( x == heights[0].length - 1 || z > heights[y][x + 1] ) {
			byte deltaZ = ( x == heights[0].length - 1 ) ? z : (byte)(z - heights[y][x + 1]);
			
			Quad south = new Quad( "East" + x + y, BLOCK_SIZE, MapNode.TILE_HEIGHT * deltaZ );
			FloatBuffer buffer = south.getTextureBuffer();
			
			buffer.clear();
			buffer.put(0).put(deltaZ);
			buffer.put(0).put(0);
			buffer.put(1).put(0);
			buffer.put(1).put(deltaZ);
			buffer.rewind();
			
			if( ! batchMap.containsKey( textureId + "_EAST" ) ) {
				GeometryBatch batch = new GeometryBatch( textureId + "_EAST", new Box("Base" + textureId,new Vector3f(0,0,0), new Vector3f(0,0,0) ) );
				batch.setLocalRotation( eastRotationQuaternion );
				
				TextureState textureState = TextureStateFactory.getInstance().getTextureState( (int)textureId );
				CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
				cullState.setCullMode( CullState.CS_BACK );
				
				if( textureState != null && cullState != null ) {
					batch.setRenderState( textureState );
					batch.setRenderState( cullState );
					batch.updateRenderState();
				}
				
				batchMap.put( textureId + "_EAST", batch );
			}
			
			batchMap.get( textureId + "_EAST" ).addGeometry( south,
					new Vector3f( (y * BLOCK_SIZE + yBatch * BLOCK_SIZE ) - macroWidth,
								  - (z * MapNode.TILE_HEIGHT - MapNode.TILE_HEIGHT  * deltaZ / 2 ),
								  (x * BLOCK_SIZE - xBatch * BLOCK_SIZE) + BLOCK_SIZE /2 ) );
		}
	}
	
	/**
	 * Builds the geometry batches for the top textures
	 * 
	 * @param topTextures
	 * @param heights
	 * @return
	 */
	private static Node buildTopBatches( byte[][] topTextures, byte[][] heights ) {
		if( topTextures.length == 0 || topTextures[0].length == 0 ) {
			return new Node( "BATCH_ROOT" );
		}
		
		Node batchRoot = new Node( "BATCH_ROOT" );
		
		int width = topTextures[0].length;
		int height = topTextures.length;
		int batchesWidth = width % BATCH_WIDTH == 0 ? width / BATCH_WIDTH : width / BATCH_WIDTH + 1;
		int batchesHeight = height % BATCH_HEIGHT == 0 ? height / BATCH_HEIGHT : height / BATCH_HEIGHT + 1; 
		
		// Cannot create a generic array of <String, GeometryBatch> =/
		Object[][] batches = new Object[batchesHeight][batchesWidth];
		
		for( int k = 0; k < batchesHeight; k++ ) {
			for( int i = 0; i < batchesWidth; i++ ) {
				batches[k][i] = new HashMap<Integer, GeometryBatch>();
			}
		}
		
		// Filling the batches
		for( int x = 0; x < width; x++ ) {
			for( int y = 0; y < height; y++ ) {
				byte textureId = topTextures[y][x];
				
				// Creating quad and setting its texture buffer
				Quad quad = new Quad( "blk " + x + "," + y, BLOCK_SIZE, BLOCK_SIZE);
				
				FloatBuffer buffer = quad.getTextureBuffer();
				buffer.clear();
				buffer.put(0).put(1);
				buffer.put(0).put(0);
				buffer.put(1).put(0);
				buffer.put(1).put(1);
				buffer.rewind();
				
				// Adding it to its batch
				int xBatch = x/BATCH_WIDTH;
				int yBatch = y/BATCH_HEIGHT;
				Map<Integer, GeometryBatch> batchMap = (Map<Integer, GeometryBatch>) batches[yBatch][xBatch];
				
				// If the batch does not exist, create and initialize it
				if( ! batchMap.containsKey( (int)textureId ) ) {
					batchMap.put( new Integer( textureId ), new GeometryBatch( "TOP " + textureId, new Box("Base" + textureId, new Vector3f(0,0,0), new Vector3f(0,0,0) ) ) );
					GeometryBatch batch = batchMap.get( (int)textureId );
					
					TextureState textureState = TextureStateFactory.getInstance().getTextureState( (int)textureId );		
					CullState cullState = DisplaySystem.getDisplaySystem().getRenderer().createCullState();
					cullState.setCullMode( CullState.CS_BACK );
					
					if( textureState != null && cullState != null ) {
						batch.setRenderState( textureState );
						batch.setRenderState( cullState );
						batch.updateRenderState();
					}
					
					batchMap.get( (int)textureId ).updateRenderState();
				}
				
				batchMap.get( (int)textureId ).addGeometry( quad, new Vector3f( (x - xBatch) * BLOCK_SIZE, (width - y - yBatch) * BLOCK_SIZE,  heights[y][x] * MapNode.TILE_HEIGHT ) );
			}
		}
		
		// Attaching batches to root
		for( int y = 0; y < batchesHeight; y++ ) {
			for( int x = 0; x < batchesWidth; x++ ) {
				Map<Integer, GeometryBatch> batchMap = (Map<Integer, GeometryBatch>) batches[y][x];
				
				Node batchNode = new Node("BATCH_NODE");
				Collection<GeometryBatch> geometryBatches = batchMap.values();
				
				for( GeometryBatch geometryBatch : geometryBatches ) {
					batchNode.attachChild( geometryBatch );
				}
				
				batchNode.setLocalTranslation( new Vector3f( x * BLOCK_SIZE, y * BLOCK_SIZE, 0.0f ) );
				batchNode.updateGeometricState( 0.0f, true );
				batchRoot.attachChild( batchNode );
			}
		}
		
		return batchRoot;
	}
}
